package definitions

import (
	"bytes"
	"fmt"
	"github.com/Xuanwo/gg"
	"github.com/Xuanwo/templateutils"
	log "github.com/sirupsen/logrus"
	"os"
	"path/filepath"
	"strings"
)

type genService struct {
	g           *gg.Generator
	data        Metadata
	implemented map[string]map[string]bool
}

func GenerateService(data Metadata, path string) {
	gs := genService{
		g:    gg.New(),
		data: data.Normalize(),
		implemented: map[string]map[string]bool{
			NamespaceFactory: {},
			NamespaceService: {},
			NamespaceStorage: {},
		},
	}

	gs.buildImplemented(path)

	gs.generateHeader()
	gs.generateObjectSystemMetadata()
	gs.generateStorageSystemMetadata()
	gs.generateSystemPairs()
	gs.generateFactory()

	if gs.data.Service != nil {
		gs.generateNamespace(gs.data.Service)
	}
	if gs.data.Storage != nil {
		gs.generateNamespace(gs.data.Storage)
	}

	gs.generateInit()

	err := gs.g.WriteFile(path)
	if err != nil {
		log.Fatalf("generate to %s: %v", path, err)
	}
}

func (gs *genService) buildImplemented(path string) {
	base := filepath.Dir(path)

	fi, err := os.ReadDir(base)
	if err != nil {
		// We will ignore all error returned while building implemented.
		return
	}
	for _, v := range fi {
		if v.IsDir() || !strings.HasSuffix(v.Name(), ".go") {
			continue
		}

		content, err := os.ReadFile(filepath.Join(base, v.Name()))
		if err != nil {
			return
		}

		source, err := templateutils.ParseContent(path, content)
		if err != nil {
			log.Fatalf("read file %s: %v", path, err)
		}
		for _, v := range source.Methods {
			if v.Recv == nil {
				continue
			}

			rt := ""
			switch v.Recv.Type {
			case "*Service":
				rt = NamespaceService
			case "*Storage":
				rt = NamespaceStorage
			case "*Factory":
				rt = NamespaceFactory
			default:
				// Ignore other methods.
				continue
			}
			gs.implemented[rt][templateutils.ToSnack(v.Name)] = true
		}
	}
}

func (gs *genService) generateHeader() {
	f := gs.g.NewGroup()

	f.AddLineComment("Code generated by go generate via cmd/definitions; DO NOT EDIT.")
	f.AddPackage(gs.data.Name)
	f.NewImport().
		AddPath("context").
		AddPath("io").
		AddPath("net/http").
		AddPath("strings").
		AddPath("time").
		AddPath("errors").
		AddLine().
		AddPath("go.beyondstorage.io/v5/services").
		AddPath("go.beyondstorage.io/v5/types")

	f.NewVar().
		AddDecl("_", "types.Storager").
		AddDecl("_", "services.ServiceError").
		AddDecl("_", "strings.Reader").
		AddDecl("_", "time.Duration").
		AddDecl("_", "http.Request")

	f.AddLineComment("Type is the type for %s", gs.data.Name)
	f.NewConst().AddField("Type", gg.Lit(gs.data.Name).String())
}

func (gs *genService) generateObjectSystemMetadata() {
	f := gs.g.NewGroup()
	data := gs.data

	f.AddLineComment("ObjectSystemMetadata stores system metadata for object.")
	osm := f.NewStruct("ObjectSystemMetadata")
	for _, info := range SortInfos(data.Infos) {
		if info.Namespace != NamespaceObject {
			continue
		}
		pname := templateutils.ToPascal(info.Name)
		// FIXME: we will support comment on field later.
		osm.AddField(pname, info.Type.FullName(data.Name))
	}

	f.AddLineComment(`
GetObjectSystemMetadata will get ObjectSystemMetadata from Object.

- This function should not be called by service implementer.
- The returning ObjectServiceMetadata is read only and should not be modified.
`)
	gosmfn := f.NewFunction("GetObjectSystemMetadata")
	gosmfn.AddParameter("o", "*types.Object").
		AddResult("", "ObjectSystemMetadata")
	gosmfn.AddBody(
		gg.S("sm, ok := o.GetSystemMetadata()"),
		gg.If(gg.S("ok")).
			AddBody(gg.Return("sm.(ObjectSystemMetadata)")),
		gg.Return(gg.Value("ObjectSystemMetadata")),
	)

	f.AddLineComment(`
setObjectSystemMetadata will set ObjectSystemMetadata into Object.

- This function should only be called once, please make sure all data has been written before set.
`)

	sosmfn := f.NewFunction("setObjectSystemMetadata")
	sosmfn.AddParameter("o", "*types.Object").
		AddParameter("sm", "ObjectSystemMetadata")
	sosmfn.AddBody(
		gg.S("o.SetSystemMetadata(sm)"),
	)
}

func (gs *genService) generateStorageSystemMetadata() {
	f := gs.g.NewGroup()
	data := gs.data

	// Generate storage system metadata.
	f.AddLineComment("StorageSystemMetadata stores system metadata for object.")
	ssm := f.NewStruct("StorageSystemMetadata")
	for _, info := range SortInfos(data.Infos) {
		if info.Namespace != NamespaceStorage {
			continue
		}
		pname := templateutils.ToPascal(info.Name)
		// FIXME: we will support comment on field later.
		ssm.AddField(pname, info.Type.FullName(data.Name))
	}

	f.AddLineComment(`
GetStorageSystemMetadata will get StorageSystemMetadata from Storage.

- This function should not be called by service implementer.
- The returning StorageServiceMetadata is read only and should not be modified.
`)
	gssmfn := f.NewFunction("GetStorageSystemMetadata")
	gssmfn.AddParameter("s", "*types.StorageMeta").
		AddResult("", "StorageSystemMetadata")
	gssmfn.AddBody(
		gg.S("sm, ok := s.GetSystemMetadata()"),
		gg.If(gg.S("ok")).
			AddBody(gg.Return("sm.(StorageSystemMetadata)")),
		gg.Return(gg.Value("StorageSystemMetadata")),
	)

	f.AddLineComment(`setStorageSystemMetadata will set StorageSystemMetadata into Storage.

- This function should only be called once, please make sure all data has been written before set.`)
	sssmfn := f.NewFunction("setStorageSystemMetadata")
	sssmfn.AddParameter("s", "*types.StorageMeta").
		AddParameter("sm", "StorageSystemMetadata")
	sssmfn.AddBody(
		gg.S("s.SetSystemMetadata(sm)"),
	)
}

func (gs *genService) generateSystemPairs() {
	f := gs.g.NewGroup()
	data := gs.data

	for _, pair := range SortPairs(data.Pairs) {
		pname := templateutils.ToPascal(pair.Name)

		f.AddLineComment(`With%s will apply %s value to Options.

%s`, pname, pair.Name, pair.Description)
		fn := f.NewFunction("With" + pname)

		// Set to true as default.
		value := "true"
		// bool type pairs don't need input.
		if pair.Type.Name != "bool" {
			fn.AddParameter("v", pair.Type.FullName(data.Name))
			value = "v"
		}
		fn.AddResult("", "types.Pair")
		fn.AddBody(gg.Return(
			gg.Value("types.Pair").
				AddField("Key", gg.Lit(pair.Name)).
				AddField("Value", value)))
	}
}

func (gs *genService) generateNamespace(ns Namespace) {
	f := gs.g.NewGroup()

	nsNameP := templateutils.ToPascal(ns.Name())

	// Generate interface assert.
	inters := f.NewVar()
	inters.AddTypedField(
		"_", gg.S("types.%s", nsNameP+"r"), gg.S("&%s{}", nsNameP))

	// Generate feature struct.
	featureStructName := nsNameP + "Features"
	f.AddLineComment("Deprecated: Use types.%s instead.", featureStructName)
	f.AddTypeAlias(featureStructName, "types."+featureStructName)

	// Generate default pairs.
	defaultStructName := fmt.Sprintf("Default%sPairs", nsNameP)
	f.AddLineComment("Deprecated: Use types.%s instead.", defaultStructName)
	f.AddTypeAlias(defaultStructName, "types."+defaultStructName)

	// Generate Features().
	f.NewFunction("Features").
		WithReceiver("s", "*"+nsNameP).
		AddResult("", "types."+featureStructName).
		AddBody(gg.Return("s.features"))

	for _, op := range ns.Operations() {
		gs.generateFunctionPairs(ns, op)
		gs.generateFunction(ns, op)
	}
}

func (gs *genService) generateFactory() {
	f := gs.g.NewGroup()
	fd := gs.data.Factory

	st := f.NewStruct("Factory")
	for _, v := range SortPairs(fd) {
		st.AddField(templateutils.ToPascal(v.Name), v.Type.FullName(gs.data.Name))
	}

	pm := make(map[string]bool)
	for _, v := range fd {
		pm[v.Name] = true
	}

	fromString := f.NewFunction("FromString").
		WithReceiver("f", "*Factory").
		AddParameter("conn", "string").
		AddResult("err", "error")
	fromString.AddBody(
		gg.S(`	slash := strings.IndexByte(conn, '/')
	question := strings.IndexByte(conn, '?')

	var partService, partStorage, partParams string

	if question != -1 {
		if len(conn) > question {
			partParams = conn[question+1:]
		}
		conn = conn[:question]
	}

	if slash != -1 {
		partService = conn[:slash]
		partStorage = conn[slash:]
	} else {
		partService = conn
	}
`))
	parseService := gg.If(`partService != ""`)
	if pm["credential"] && pm["endpoint"] {
		parseService.AddBody(
			gg.S(`at := strings.IndexByte(partService, '@')
		if at == -1 {
			f.Endpoint = partService
		} else {
			xs := strings.SplitN(partService, "@", 2)
			f.Credential, f.Endpoint = xs[0], xs[1]
		}`))
	} else if pm["credential"] {
		parseService.AddBody(gg.S("f.Credential = partService"))
	} else if pm["endpoint"] {
		parseService.AddBody(gg.S("f.Endpoint = partService"))
	}
	fromString.AddBody(parseService)

	parseStorage := gg.If(`partStorage != ""`)
	// WorkDir is required.
	if pm["name"] {
		parseStorage.AddBody(gg.S(`slash := strings.IndexByte(partStorage[1:], '/')
		if slash == -1 {
			f.Name = partStorage[1:]
		} else {
			f.Name, f.WorkDir = partStorage[1:slash+1], partStorage[slash+1:]
		}
`))
	} else {
		parseStorage.AddBody(gg.S("f.WorkDir = partStorage"))
	}
	fromString.AddBody(parseStorage)

	parseParams := gg.If(`partParams != ""`)
	parseParams.AddBody(
		gg.S(`xs := strings.Split(partParams, "&")`),
		gg.For("_, v := range xs").AddBody(
			gg.S(`var key, value string
			vs := strings.SplitN(v, "=", 2)
			key = vs[0]
			if len(vs) > 1 {
				value = vs[1]
			}`),
			gg.Embed(func() gg.Node {
				s := gg.Switch("key")

				for _, v := range SortPairs(fd) {
					nameP := templateutils.ToPascal(v.Name)

					ca := s.NewCase(gg.Lit(v.Name))
					switch v.Type.Name {
					case "bool":
						ca.AddBody(gg.S("f.%s = true", nameP))
					case "string":
						ca.AddBody(gg.S("f.%s = value", nameP))
					}
				}
				return s
			}),
		),
	)

	fromString.AddBody(parseParams)

	fromString.AddBody(gg.Return("nil"))

	withPairs := f.NewFunction("WithPairs").
		WithReceiver("f", "*Factory").
		AddParameter("ps", "...types.Pair").
		AddResult("err", "error")
	withPairs.AddBody(
		gg.For("_, v := range ps").AddBody(gg.Embed(func() gg.Node {
			s := gg.Switch("v.Key")

			for _, v := range SortPairs(fd) {
				nameP := templateutils.ToPascal(v.Name)

				s.NewCase(gg.Lit(v.Name)).AddBody(
					gg.S("f.%s = v.Value.(%s)",
						nameP, v.Type.FullName(gs.data.Name)))
			}

			return s
		})),
		gg.Return("nil"),
	)

	fromMap := f.NewFunction("FromMap").
		WithReceiver("f", "*Factory").
		AddParameter("m", "map[string]interface{}").
		AddResult("err", "error")
	fromMap.AddBody(`return errors.New("FromMap not implemented")`)

	// Generate NewServicer
	newServicer := f.NewFunction("NewServicer").
		WithReceiver("f", "*Factory").
		AddResult("srv", "types.Servicer").
		AddResult("err", "error")
	if gs.data.Service == nil {
		newServicer.AddBody(`return nil, errors.New("servicer not implemented")`)
	} else if !gs.implemented[NamespaceFactory]["new_service"] {
		log.Error("We should implement operation [newService], but not")
		log.Infof("Please implement them like the following:")

		println("---")
		println("func (f *Factory) newService() (*Service, error) {}")
		println("---")
	} else {
		newServicer.AddBody("return f.newService()")
	}

	// Generate NewStorager
	newStorager := f.NewFunction("NewStorager").
		WithReceiver("f", "*Factory").
		AddResult("sto", "types.Storager").
		AddResult("err", "error")
	if gs.data.Storage == nil {
		newStorager.AddBody(`return nil, errors.New("storager not implemented")`)
	} else if !gs.implemented[NamespaceFactory]["new_storage"] {
		log.Error("We should implement operation [newStorage], but not")
		log.Infof("Please implement them like the following:")

		println("---")
		println("func (f *Factory) newStorage() (*Storage, error) {}")
		println("---")
	} else {
		newStorager.AddBody("return f.newStorage()")
	}

	serviceFeatures := f.NewFunction("serviceFeatures").
		WithReceiver("f", "*Factory").
		AddResult("s", "types.ServiceFeatures")
	for _, fe := range gs.data.Service.ListFeatures(FeatureTypeOperation, FeatureTypeSystem) {
		if gs.data.Service.HasFeature(fe.Name) {
			serviceFeatures.AddBody(gg.S("s.%s = true", templateutils.ToPascal(fe.Name)))
		}
	}
	for _, fe := range gs.data.Service.ListFeatures(FeatureTypeVirtual) {
		serviceFeatures.AddBody(
			gg.If("f." + templateutils.ToPascal("enable_"+fe.Name)).AddBody(
				gg.S("s.%s = true", templateutils.ToPascal(fe.Name))))
	}
	serviceFeatures.AddBody(gg.Return())

	storageFeatures := f.NewFunction("storageFeatures").
		WithReceiver("f", "*Factory").
		AddResult("s", "types.StorageFeatures")
	for _, fe := range gs.data.Storage.ListFeatures(FeatureTypeOperation, FeatureTypeSystem) {
		if gs.data.Storage.HasFeature(fe.Name) {
			storageFeatures.AddBody(gg.S("s.%s = true", templateutils.ToPascal(fe.Name)))
		}
	}
	for _, fe := range gs.data.Storage.ListFeatures(FeatureTypeVirtual) {
		storageFeatures.AddBody(
			gg.If("f." + templateutils.ToPascal("enable_"+fe.Name)).AddBody(
				gg.S("s.%s = true", templateutils.ToPascal(fe.Name))))
	}
	storageFeatures.AddBody(gg.Return())
}

func (gs *genService) generateFunctionPairs(ns Namespace, op Operation) {
	f := gs.g.NewGroup()

	nsNameP := templateutils.ToPascal(ns.Name())
	fnNameP := templateutils.ToPascal(op.Name)

	// Generate pair struct
	pairStructName := fmt.Sprintf("pair%s%s", nsNameP, fnNameP)
	pairStruct := f.NewStruct(pairStructName).
		AddField("pairs", "[]types.Pair")
	for _, pair := range ns.ListPairs(op.Name) {
		pairNameP := templateutils.ToPascal(pair.Name)
		pairStruct.AddField("Has"+pairNameP, "bool")
		pairStruct.AddField(pairNameP, pair.Type.FullName(ns.Name()))
	}

	// Generate parse pair function.
	pairParseName := fmt.Sprintf("parsePair%s%s", nsNameP, fnNameP)
	pairParse := f.NewFunction(pairParseName).
		WithReceiver("s", "*"+nsNameP).
		AddParameter("opts", "[]types.Pair").
		AddResult("", pairStructName).
		AddResult("", "error")

	pairParse.AddBody(
		gg.S("result :="),
		gg.Value(pairStructName).AddField("pairs", "opts"),
		gg.Line(),
		gg.For(gg.S("_, v := range opts")).
			AddBody(gg.Embed(func() gg.Node {
				is := gg.Switch(gg.S("v.Key"))

				for _, pair := range ns.ListPairs(op.Name) {
					pairNameP := templateutils.ToPascal(pair.Name)
					is.NewCase(gg.Lit(pair.Name)).AddBody(
						gg.If(gg.S("result.Has%s", pairNameP)).
							AddBody(gg.Continue()),
						gg.S("result.Has%s = true", pairNameP),
						gg.S("result.%s = v.Value.(%s)", pairNameP, pair.Type.FullName(ns.Name())),
					)
				}
				dcas := is.NewDefault()
				if ns.HasFeature("loose_pair") {
					dcas.AddBody(
						gg.LineComment(`
loose_pair feature introduced in GSP-109.
If user enable this feature, service should ignore not support pair error.`),
						gg.If(gg.S("s.features.LoosePair")).
							AddBody(gg.Continue()),
					)
				}
				dcas.AddBody(gg.S("return pair%s%s{}, services.PairUnsupportedError{Pair:v}", nsNameP, fnNameP))
				return is
			})),
		gg.Return("result", "nil"),
	)
}

func (gs *genService) generateFunction(ns Namespace, op Operation) {
	nsNameP := templateutils.ToPascal(ns.Name())
	fnNameP := templateutils.ToPascal(op.Name)

	data := gs.data
	f := gs.g.NewGroup()
	shouldImplemented := ns.HasFeature(op.Name)

	// Check if user really implement this function.
	if shouldImplemented && !gs.implemented[ns.Name()][op.Name] {
		log.Errorf("We should implement operation [%s], but not", op.Name)
		log.Infof("Please update features or implement them like the following:")

		var buf bytes.Buffer
		fnNameC := templateutils.ToCamel(op.Name)
		xg := gg.New()
		gfn := xg.NewGroup().NewFunction(fnNameC).
			WithReceiver("s", "*"+nsNameP)

		ctxField := getField("ctx")
		if !op.Local {
			gfn.AddParameter(ctxField.Name, ctxField.Type.FullName(ns.Name()))
		}
		for _, v := range op.Params {
			// We need to remove pair from generated functions.
			if v.Name == "pairs" {
				continue
			}
			gfn.AddParameter(v.Name, v.Type.FullName(ns.Name()))
		}
		gfn.AddParameter("opt", "pair"+nsNameP+fnNameP)
		for _, v := range op.Results {
			gfn.AddResult(v.Name, v.Type.FullName(ns.Name()))
		}
		xg.Write(&buf)

		println("---")
		println(buf.String())
		println("---")
	}

	// Generate a local function.
	if op.Local {
		xfn := f.NewFunction(fnNameP).WithReceiver("s", "*"+nsNameP)
		for _, field := range op.Params {
			xfn.AddParameter(field.Name, field.Type.FullName(data.Name))
		}
		for _, field := range op.Results {
			xfn.AddResult(field.Name, field.Type.FullName(data.Name))
		}
		if shouldImplemented {
			xfn.AddBody(
				gg.S("pairs = append(pairs, s.defaultPairs.%s...)", fnNameP),
				gg.S("var opt pair%s%s", nsNameP, fnNameP),
				gg.Line(),
				gg.LineComment("Ignore error while handling local functions."),
				gg.S("opt, _ = s.parsePair%s%s(pairs)", nsNameP, fnNameP),
				gg.Return(
					gg.Embed(func() gg.Node {
						ic := gg.Call(templateutils.ToCamel(op.Name)).
							WithOwner("s")
						for _, v := range op.Params {
							// We don't need to call pair again.
							if v.Name == "pairs" {
								continue
							}
							ic.AddParameter(v.Name)
						}
						ic.AddParameter("opt")
						return ic
					})))
		} else {
			xfn.AddBody(
				gg.Return(),
			)
		}

		return
	}

	// Generate non-local function like `Write`
	// Write will call `WriteWithContext` internally.
	xfn := f.NewFunction(fnNameP).
		WithReceiver("s", "*"+nsNameP)
	for _, field := range op.Params {
		xfn.AddParameter(field.Name, field.Type.FullName(data.Name))
	}
	for _, field := range op.Results {
		xfn.AddResult(field.Name, field.Type.FullName(data.Name))
	}
	if shouldImplemented {
		xfn.AddBody(
			"ctx := context.Background()",
			gg.Return(
				gg.Embed(func() gg.Node {
					ic := gg.Call(fnNameP + "WithContext").
						WithOwner("s")
					ic.AddParameter("ctx")
					for _, v := range op.Params {
						if v.Name == "pairs" {
							ic.AddParameter("pairs...")
							continue
						}
						ic.AddParameter(v.Name)
					}
					return ic
				})))
	} else {
		xfn.AddBody(
			gg.S(`err = types.NewOperationNotImplementedError("%s")`, op.Name),
			gg.Return(),
		)
	}

	// Generate non-local function like `WriteWithContext`
	xfn = f.NewFunction(fnNameP+"WithContext").
		WithReceiver("s", "*"+nsNameP)
	xfn.AddParameter("ctx", "context.Context")
	for _, field := range op.Params {
		xfn.AddParameter(field.Name, field.Type.FullName(data.Name))
	}
	for _, field := range op.Results {
		xfn.AddResult(field.Name, field.Type.FullName(data.Name))
	}
	if shouldImplemented {
		xfn.AddBody(
			gg.Defer(gg.Embed(func() gg.Node {
				caller := gg.Call("formatError").WithOwner("s")
				caller.AddParameter(gg.Lit(op.Name)).AddParameter("err")
				isEmpty := true
				for _, v := range op.Params {
					// formatError only accept string as input.
					if v.Type.Name != "string" {
						continue
					}
					caller.AddParameter(v.Name)
					isEmpty = false
				}
				if isEmpty && ns.Name() == NamespaceService {
					caller.AddParameter("\"\"")
				}

				fn := gg.Function("").
					AddBody(gg.S("err = "), caller).WithCall()
				return fn
			})),
			gg.S("pairs = append(pairs, s.defaultPairs.%s...)", fnNameP),
			gg.S("var opt pair%s%s", nsNameP, fnNameP),
			gg.Line(),
			gg.S("opt, err = s.parsePair%s%s(pairs)", nsNameP, fnNameP),
			gg.If(gg.S("err != nil")).AddBody(gg.Return()),
			gg.Return(
				gg.Embed(func() gg.Node {
					ic := gg.Call(templateutils.ToCamel(op.Name)).
						WithOwner("s")
					ic.AddParameter("ctx")
					for _, v := range op.Params {
						// We don't need to call pair again.
						if v.Name == "pairs" {
							continue
						}
						if v.Name == "path" || v.Name == "src" || v.Name == "dst" || v.Name == "target" {
							ic.AddParameter(gg.S(`strings.ReplaceAll(%s, "\\", "/")`, v.Name))
							continue
						}
						ic.AddParameter(v.Name)
					}
					ic.AddParameter("opt")
					return ic
				})))
	} else {
		xfn.AddBody(
			gg.S(`err = types.NewOperationNotImplementedError("%s")`, op.Name),
			gg.Return(),
		)
	}
}
func (gs *genService) generateInit() {
	f := gs.g.NewGroup()

	f.NewFunction("init").AddBody("services.RegisterFactory(Type, &Factory{})")
}
